/*
 * Copyright 1999-2101 Alibaba Group.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.alibaba.fastjson.parser;

import static com.alibaba.fastjson.parser.TypeUtils.castToBigDecimal;
import static com.alibaba.fastjson.parser.TypeUtils.castToBigInteger;
import static com.alibaba.fastjson.parser.TypeUtils.castToBoolean;
import static com.alibaba.fastjson.parser.TypeUtils.castToByte;
import static com.alibaba.fastjson.parser.TypeUtils.castToDate;
import static com.alibaba.fastjson.parser.TypeUtils.castToDouble;
import static com.alibaba.fastjson.parser.TypeUtils.castToFloat;
import static com.alibaba.fastjson.parser.TypeUtils.castToInt;
import static com.alibaba.fastjson.parser.TypeUtils.castToLong;
import static com.alibaba.fastjson.parser.TypeUtils.castToShort;
import static com.alibaba.fastjson.parser.TypeUtils.castToString;

import java.lang.reflect.Array;
import java.lang.reflect.Method;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.lang.reflect.TypeVariable;
import java.lang.reflect.WildcardType;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;

import com.alibaba.fastjson.JSONArray;
import com.alibaba.fastjson.JSONException;
import com.alibaba.fastjson.JSONObject;

/**
 * 
 * @author wenshao<szujobs@hotmail.com>
 * 
 */
@SuppressWarnings("rawtypes")
public class DefaultExtJSONParser extends DefaultJSONParser implements ExtJSONParser {
	private JavaBeanMapping mapping;
	
	private boolean ignoreNotMatch = true;
	
	private final static Set<Class<?>> primitiveClasses = new HashSet<Class<?>>();
	static {
		primitiveClasses.add(boolean.class);
		primitiveClasses.add(byte.class);
		primitiveClasses.add(short.class);
		primitiveClasses.add(int.class);
		primitiveClasses.add(long.class);
		primitiveClasses.add(float.class);
		primitiveClasses.add(double.class);
		
		primitiveClasses.add(Boolean.class);
		primitiveClasses.add(Byte.class);
		primitiveClasses.add(Short.class);
		primitiveClasses.add(Integer.class);
		primitiveClasses.add(Long.class);
		primitiveClasses.add(Float.class);
		primitiveClasses.add(Double.class);
		
		primitiveClasses.add(BigInteger.class);
		primitiveClasses.add(BigDecimal.class);
		primitiveClasses.add(String.class);
	}

	public DefaultExtJSONParser(String input) {
		this(input, JavaBeanMapping.getGlobalInstance());
	}
	
	public DefaultExtJSONParser(String input, JavaBeanMapping mapping) {
		super(input);
		this.mapping = mapping;
	}

	public DefaultExtJSONParser(String input, SymbolTable symbolTable) {
		this(input, symbolTable, JavaBeanMapping.getGlobalInstance());
	}
	
	public DefaultExtJSONParser(String input, SymbolTable symbolTable, JavaBeanMapping mapping) {
		super(input, symbolTable);
		this.mapping = mapping;
	}
	
	public DefaultExtJSONParser(String input, SymbolTable symbolTable, int features) {
		this(input, symbolTable, features, JavaBeanMapping.getGlobalInstance());
	}
	
	public DefaultExtJSONParser(String input, SymbolTable symbolTable, int features, JavaBeanMapping mapping) {
		super(input, symbolTable, features);
		this.mapping = mapping;
	}
	
	public JavaBeanMapping getMapping() {
		return mapping;
	}

	public void setMapping(JavaBeanMapping mapping) {
		this.mapping = mapping;
	}

	public boolean isIgnoreNotMatch() {
		return ignoreNotMatch;
	}

	public void setIgnoreNotMatch(boolean ignoreNotMatch) {
		this.ignoreNotMatch = ignoreNotMatch;
	}

	@SuppressWarnings({ "unchecked" })
	public <T> T parseObject(Class<T> clazz) {
		if (lexer.token() == JSONToken.NULL) {
			lexer.nextToken();
			return null;
		}
		
		try {
			if (clazz.isEnum()) {
				return (T) parseEnum((Class<Enum>) clazz);
			} else if (clazz == Map.class || clazz == HashMap.class) {
				Map<String, Object> map = new HashMap<String, Object>();
				parseObject(map);
				return (T) map;
			} else if (clazz == LinkedHashMap.class) {
				Map<String, Object> map = new LinkedHashMap<String, Object>();
				parseObject(map);
				return (T) map;
			} else if (clazz == java.util.Date.class) {
				Object val = parse();
				if (val instanceof java.util.Date) {
					return (T) val;
				} else if (val instanceof Number) {
					val = new java.sql.Date(((Number) val).longValue());
				} else {
					throw new JSONException("parse error");
				}
				return (T) val;
			} else if (clazz == java.sql.Timestamp.class) {
				Object val = parse();
				if (val instanceof java.util.Date) {
					val = new java.sql.Timestamp(((Date) val).getTime());
				} else if (val instanceof Number) {
					val = new java.sql.Timestamp(((Number) val).longValue());
				} else {
					throw new JSONException("parse error");
				}
				
				return (T) val;
			} else if (clazz == java.sql.Date.class) {
				Object val = parse();
				if (val instanceof java.util.Date) {
					val = new java.sql.Date(((Date) val).getTime());
				} else if (val instanceof Number) {
					val = new java.sql.Date(((Number) val).longValue());
				} else {
					throw new JSONException("parse error");
				}
				
				return (T) val;
			} else if (clazz == JSONObject.class) {
				return (T) parseObject();
			} else if (clazz == JSONArray.class) {
				JSONArray array = new JSONArray();
				parseArray(array);
				return (T) array;
			} else if (clazz.isArray()) {
				JSONArray array = new JSONArray();
				parseArray(array);
				int size = array.size();
				Class<?> componentType = clazz.getComponentType();
				Object objArray = Array.newInstance(componentType, size);
				for (int i = 0; i < size; ++i) {
					Object value = array.get(i);
					Object element = TypeUtils.cast(value, componentType, mapping);
					Array.set(objArray, i, element);
				}
				return (T) objArray; // TODO
			}  else if (clazz == Object.class) {
				return (T) parse();
			}  else if (primitiveClasses.contains(clazz)) {
				Object jsonValue = parse();
				return (T) TypeUtils.cast(jsonValue, clazz, mapping);
			} else {
				T value = clazz.newInstance();
				parseObject(value);
				return value;
			}

		} catch (JSONException e) {
			throw e;
		} catch (Throwable e) {
			throw new JSONException(e.getMessage(), e);
		}
	}

	@SuppressWarnings({ "unchecked" })
	public <T> T parseEnum(Class<T> eClass) {
		try {
			Object value = parse();

			if (value instanceof String) {
				return (T) Enum.valueOf((Class<Enum>) eClass, (String) value);
			} else if (value instanceof Integer) {
				initEnum((Class<Enum>)eClass);
				Map<Integer, Enum> items = mapping.getEnumValues().get(eClass);

				T e = (T) items.get(value);
				if (e == null) {
					throw new JSONException("parse enum " + eClass.getName() + " error, value : " + value);
				}
				return e;
			} else {
				throw new JSONException("parse enum " + eClass.getName() + " error, value : " + value);
			}
		} catch (JSONException e) {
			throw e;
		} catch (Throwable e) {
			throw new JSONException(e.getMessage(), e);
		}
	}

	public void initEnum(Class<? extends Enum> clazz) throws Exception {
		Map<Integer, Enum> items = mapping.getEnumValues().get(clazz);

		if (items == null) {
			items = new HashMap<Integer, Enum>();

			Method method = clazz.getMethod("values");
			Object[] values = (Object[]) method.invoke(null);
			for (Object value : values) {
				Enum e = (Enum) value;
				items.put(e.ordinal(), e);
			}

			mapping.getEnumValues().putIfAbsent(clazz, items);
			items = mapping.getEnumValues().get(clazz);
		}
	}

	public <T> List<T> parseArray(Class<T> clazz) {
		List<T> array = new ArrayList<T>();
		parseArray(clazz, array);
		return array;
	}

	@SuppressWarnings({ "unchecked" })
	public void parseArray(Class<?> clazz, Collection array) {
		accept(JSONToken.LBRACKET);

		for (;;) {
			if (isEnabled(Feature.AllowArbitraryCommas)) {
				while (lexer.token() == JSONToken.COMMA) {
					lexer.nextToken();
					continue;
				}
			}
			
			if (lexer.token() == JSONToken.RBRACKET) {
				break;
			}

			Object val = parseObject(clazz);
			array.add(val);

			if (lexer.token() == JSONToken.COMMA) {
				lexer.nextToken();
				continue;
			}
		}

		accept(JSONToken.RBRACKET);
	}

	// 这个方法做了很多优化，所以结构不是很好
	public void parseObject(Object object) {
		Class<?> clazz = object.getClass();
		Map<String, Method> setters = mapping.getSetters(clazz);

		if (lexer.token() != JSONToken.LBRACE) {
			throw new JSONException("syntax error, expect {, actual " + lexer.token());
		}

		for (;;) {
			lexer.skipWhitespace();
			char ch = lexer.getCurrent();
			if (isEnabled(Feature.AllowArbitraryCommas)) {
			    while (ch == ',') {
			        lexer.incrementBufferPosition();
			        lexer.skipWhitespace();
			        ch = lexer.getCurrent();
			    }
			}

			String key;
			if (ch == '"') {
				key = lexer.scanSymbol(symbolTable, '"');
				lexer.skipWhitespace();
				ch = lexer.getCurrent();
				if (ch != ':') {
					throw new JSONException("expect ':' at " + lexer.pos());
				}
			} else if (ch == '}') {
				lexer.incrementBufferPosition();
				lexer.resetStringPosition();
				return;
			} else if (ch == '\'') {
				if (!isEnabled(Feature.AllowSingleQuotes)) {
					throw new JSONException("syntax error");
				}

				key = lexer.scanSymbol(symbolTable, '\'');
				lexer.skipWhitespace();
				ch = lexer.getCurrent();
				if (ch != ':') {
					throw new JSONException("expect ':' at " + lexer.pos());
				}
			} else {
				if (!isEnabled(Feature.AllowUnQuotedFieldNames)) {
					throw new JSONException("syntax error");
				}

				key = lexer.scanSymbolUnQuoted(symbolTable);
				lexer.skipWhitespace();
				ch = lexer.getCurrent();
				if (ch != ':') {
					throw new JSONException("expect ':' at " + lexer.pos() + ", actual " + ch);
				}
			}

			lexer.incrementBufferPosition();
			lexer.skipWhitespace();
			ch = lexer.getCurrent();

			lexer.resetStringPosition();

			Method method = setters.get(key);
			if (method == null) {
				if (!ignoreNotMatch) {
					throw new JSONException("setter not found, class " + clazz.getName() + ", property " + key);
				}

				lexer.nextToken();
				parse(); // skip

				if (lexer.token() == JSONToken.RBRACE) {
					lexer.nextToken();
					return;
				}
			} else {
				try {
					Class<?> propertyType = method.getParameterTypes()[0];

					if (ch == '"' && propertyType == String.class) {
						lexer.scanString();
						String value = lexer.stringVal();

						method.invoke(object, value);

					} else if ((ch >= '0' && ch <= '9' || ch == '-') && (propertyType == int.class || propertyType == Integer.class)) {
						lexer.scanNumber();

						int value = lexer.intValue();

						method.invoke(object, value);

					} else if ((ch >= '0' && ch <= '9' || ch == '-') && (propertyType == long.class || propertyType == Long.class)) {
						lexer.scanNumber();

						long value = lexer.longValue();

						method.invoke(object, value);
					} else {

						Object value;
						lexer.nextToken();

						// 专门调整顺序的
						if (propertyType.equals(String.class)) {
							value = lexer.stringVal();
							value = castToString(parse());
						} else if (propertyType.equals(int.class) || propertyType.equals(Integer.class)) {
							value = castToInt(parse());
						} else if (propertyType.equals(long.class) || propertyType.equals(Long.class)) {
							value = castToLong(parse());
						} else if (propertyType.equals(boolean.class) || propertyType.equals(Boolean.class)) {
							value = castToBoolean(parse());
						} else if (propertyType.equals(BigDecimal.class)) {
							value = castToBigDecimal(parse());
						} else if (propertyType.equals(Date.class)) {
							Object parsedValue = parse();
							if (parsedValue instanceof String) {
								String text = (String) parsedValue;
								JSONScanner dateLexer = new JSONScanner(text);
								if (dateLexer.scanISO8601DateIfMatch()) {
									value = dateLexer.getCalendar().getTime();
								} else {
									value = castToDate(parsedValue);
								}
							} else {
								value = castToDate(parsedValue);
							}
						} else if (propertyType.equals(float.class) || propertyType.equals(Float.class)) {
							value = castToFloat(parse());
						} else if (propertyType.equals(double.class) || propertyType.equals(Double.class)) {
							value = castToDouble(parse());
						} else if (Collection.class.isAssignableFrom(propertyType)) {
							Type type = method.getGenericParameterTypes()[0];

							Object argVal;
							argVal = parseArrayWithType(type);

							value = argVal;
						} else if (propertyType.equals(short.class) || propertyType.equals(Short.class)) {
							value = castToShort(parse());
						} else if (propertyType.equals(byte.class) || propertyType.equals(Byte.class)) {
							value = castToByte(parse());
						} else if (propertyType.equals(BigInteger.class)) {
							value = castToBigInteger(parse());
						} else {
							if (lexer.token() == JSONToken.NULL) {
								value = null;
								lexer.nextToken();
							} else {
								value = parseObject(propertyType);
							}
						}

						method.invoke(object, value);

						if (lexer.token() == JSONToken.RBRACE) {
							lexer.nextToken();
							return;
						}
					}
				} catch (Throwable e) {
					throw new JSONException("set proprety error, " + method.getName(), e);
				}
			}

			lexer.skipWhitespace();
			ch = lexer.getCurrent();

			if (ch == ',') {
				lexer.incrementBufferPosition();
				continue;
			} else if (ch == '}') {
				lexer.incrementBufferPosition();
				lexer.nextToken();
				return;
			}

		}
	}

	public Object parseArrayWithType(Type collectionType) {
		if (lexer.token() == JSONToken.NULL) {
			lexer.nextToken();
			return null;
		}
		
		Type actualTypeArgument = ((ParameterizedType) collectionType).getActualTypeArguments()[0];

		if (actualTypeArgument instanceof Class) {
			List<Object> array = new ArrayList<Object>();
			this.parseArray((Class) actualTypeArgument, array);
			return array;
		}

		if (actualTypeArgument instanceof WildcardType) {
			WildcardType wildcardType = (WildcardType) actualTypeArgument;

			if (wildcardType.getUpperBounds().length == 1) {
				Type upperBoundType = wildcardType.getUpperBounds()[0];
				if (upperBoundType instanceof Class) {
					if (Object.class.equals(upperBoundType)) {
						if (wildcardType.getLowerBounds().length == 0) {
							// Collection<?>
							return parse();
						} else {
							throw new JSONException("not support type : " + collectionType);
						}
					}

					List<Object> array = new ArrayList<Object>();
					this.parseArray((Class) upperBoundType, array);
					return array;
				}
			}

			// throw new JSONException("not support type : " +
			// collectionType);return parse();
		}

		if (actualTypeArgument instanceof TypeVariable) {
			TypeVariable typeVariable = (TypeVariable) actualTypeArgument;
			Type[] bounds = typeVariable.getBounds();

			if (bounds.length != 1) {
				throw new JSONException("not support : " + typeVariable);
			}

			Type boundType = bounds[0];
			if (boundType instanceof Class) {
				List<Object> array = new ArrayList<Object>();
				this.parseArray((Class) boundType, array);
				return array;
			}
		}
		
		if (actualTypeArgument instanceof ParameterizedType) {
			ParameterizedType parameterizedType = (ParameterizedType) actualTypeArgument;
			
			List<Object> array = new ArrayList<Object>();
			this.parseArray((Class) parameterizedType.getRawType(), array);
			return array;
		}

		throw new JSONException("TODO : " + collectionType);
	}
}
